package org.primefaces.optimus.config;

import java.net.JarURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.NotFoundException;

import javax.servlet.ServletContext;

import org.primefaces.optimus.config.annotations.Controller;
import org.primefaces.optimus.config.annotations.Model;

/**
 * Default implementation of AnnotationScanner
 */
public class DefaultAnnotationScanner implements AnnotationScanner {

	private static final String WEB_INF_CLASSES = "/WEB-INF/classes/";
	private static final String WEB_INF_LIB = "/WEB-INF/lib/";
	private Map<String, BeanInfo> lazyControllers = new HashMap<String, BeanInfo>();
	private Map<String, BeanInfo> startupControllers = new HashMap<String, BeanInfo>();
	private String contextScanPath = null;
	private ClassPool pool;
	
	public Map<String, BeanInfo> getLazyControllers() {
		return lazyControllers;
	}

	public Map<String, BeanInfo> getStartupControllers() {
		return startupControllers;
	}

	public void scan(ServletContext servletContext) {
		String contextScanPathValue = servletContext.getInitParameter("optimus.CONTEXT_SCAN_PATH");
		contextScanPath = contextScanPathValue.replace(".", "/") + "/";
		pool = ClassPool.getDefault();
		pool.insertClassPath(new ClassClassPath(this.getClass()));
		
		scanWebInfClasses(servletContext, WEB_INF_CLASSES + contextScanPath);
		scanWebInfJars(servletContext);
	}
	
	private void scanWebInfJars(ServletContext servletContext) {
		List<JarFile> jars = findJars(servletContext, WEB_INF_LIB);
		if(!jars.isEmpty()) {
			for(JarFile jarFile : jars) {
				scanJarFile(jarFile);
			}
		}
	}

	private void scanJarFile(JarFile jarFile) {
		Enumeration<JarEntry> entries = jarFile.entries();
		
		while(entries.hasMoreElements()) {
			JarEntry entry = entries.nextElement();
			
			if(!entry.getName().startsWith(contextScanPath))
				continue;
			
			if(entry.getName().endsWith(".class")) {
				try {
					String className = pathToClassName(entry.getName());
					CtClass cc = pool.get(className);
					
					Controller controller = getController(cc);
					
					if (controller != null) {
						if(controller.startup())
							startupControllers.put(controller.name(), new BeanInfo(cc.toClass(), controller.name(), controller.scope(), controller.startup()));
						else
							lazyControllers.put(controller.name(), new BeanInfo(cc.toClass(), controller.name(), controller.scope(), controller.startup()));
						
						
					}
				} catch(Exception exception) {
					throw new RuntimeException(exception);
				}
			}
		}
	}
	
	private List<JarFile> findJars(ServletContext servletContext, String rootPath) {
		Set<String> paths = servletContext.getResourcePaths(rootPath);
        List<JarFile> jars = new ArrayList<JarFile>();
        
        if (paths != null && !paths.isEmpty()) {
            for (String path : paths) {
                if(path.endsWith(".jar")) {
                    try {
                        String jarUrlPath = "jar:" + servletContext.getResource(path) + "!/";
                        URL url = new URL(jarUrlPath);
                        JarFile jarFile = ((JarURLConnection) url.openConnection()).getJarFile();
                        
                        jars.add(jarFile);
                    } catch (Exception e) {
                    	e.printStackTrace();
                    }
                }
            }
        }
        
        return jars;
    }

	protected void scanWebInfClasses(ServletContext servletContext, String rootPath) {
		Set<String> paths = servletContext.getResourcePaths(rootPath);
			 
		for (String path : paths) {
			if (path.endsWith("/")) {
				scanWebInfClasses(servletContext, path);
			} else {
				if (path.endsWith(".class")) {
					try {
						String className = pathToClassName(path);
						CtClass cc = pool.get(className);
						
						Controller controller = getController(cc);

						if (controller != null) {
							//enhanceController(cc, servletContext.getRealPath(WEB_INF_CLASSES));
							
							if(controller.startup())
								startupControllers.put(controller.name(), new BeanInfo(cc.toClass(), controller.name(), controller.scope(), controller.startup()));
							else
								lazyControllers.put(controller.name(), new BeanInfo(cc.toClass(), controller.name(), controller.scope(), controller.startup()));
							
						}
					} catch(Exception exception) {
						throw new RuntimeException(exception);
					}
				}
			}
		}
	}
	
	private void enhanceController(CtClass cc, String path) throws Exception {
		CtField[] fields = cc.getDeclaredFields();
		boolean enhanced = false;

		for (CtField field : fields) {
			Object[] annotations = field.getAnnotations();
			
			for (Object annotation : annotations) {
				if (annotation instanceof Model) {
					String name = field.getName();
					String capitalizedName = capitalize(name);
					String type = field.getType().getName();
					String getterMethodName = "get" + capitalizedName;
					
					//Only enhance if getter&setter is not there
					try {
						CtMethod declaredMethod = cc.getDeclaredMethod(getterMethodName);
					}catch(NotFoundException notFoundException) {
						CtMethod getter = CtNewMethod.make("public " + type + " " + getterMethodName + "() { return this." + name + "; }", cc);
						CtMethod setter = CtNewMethod.make("public void set" + capitalizedName + "(" + type + " value) { this." + name + " = value; }", cc);

						cc.addMethod(getter);
						cc.addMethod(setter);
						
						enhanced = true;
					}
				}
			}
		}

		if(enhanced)
			cc.writeFile(path);
	}
	
	private String capitalize(String name) {
		String firstLetter = name.substring(0, 1);
		String remainder = name.substring(1);
		
		return firstLetter.toUpperCase() + remainder.toLowerCase();
	}

	private Controller getController(CtClass ctClass) throws ClassNotFoundException {
		Object[] annotations = ctClass.getAnnotations();
		
		for(Object obj : annotations) {
			if(obj instanceof Controller)
				return (Controller) obj;
		}
		
		return null;
	}
	
	protected String pathToClassName(String path) {
		String classNamePath = null;
		
		if(path.indexOf(WEB_INF_CLASSES) != -1)
			classNamePath = path.substring(WEB_INF_CLASSES.length());
		else
			classNamePath = path;
		
		classNamePath = classNamePath.substring(0, (classNamePath.length() - 6));
	
		return classNamePath.replaceAll("/", ".");
	}
}